package com.hqd.ch03.v48.boot.autoconfigure.condition;

import com.hqd.ch03.utils.ObjectUtils;
import com.hqd.ch03.v48.context.annotation.Condition;
import com.hqd.ch03.v48.context.annotation.ConditionContext;
import com.hqd.ch03.v48.core.type.AnnotatedTypeMetadata;
import com.hqd.ch03.v48.factory.BeanFactory;
import com.hqd.ch03.v48.factory.ConfigurableBeanFactory;
import com.hqd.ch03.v48.factory.ConfigurableListableBeanFactory;

import java.util.*;

public final class ConditionEvaluationReport {

    private static final String BEAN_NAME = "autoConfigurationReport";

    private static final AncestorsMatchedCondition ANCESTOR_CONDITION = new AncestorsMatchedCondition();

    private final SortedMap<String, ConditionAndOutcomes> outcomes = new TreeMap<>();
    private final List<String> exclusions = new ArrayList<>();
    private final Set<String> unconditionalClasses = new HashSet<>();
    private boolean addedAncestorOutcomes;
    private ConditionEvaluationReport parent;

    /**
     * Private constructor.
     *
     * @see #get(ConfigurableListableBeanFactory)
     */
    private ConditionEvaluationReport() {
    }

    /**
     * Attempt to find the {@link ConditionEvaluationReport} for the specified bean
     * factory.
     *
     * @param beanFactory the bean factory (may be {@code null})
     * @return the {@link ConditionEvaluationReport} or {@code null}
     */
    public static ConditionEvaluationReport find(BeanFactory beanFactory) {
        if (beanFactory != null && beanFactory instanceof ConfigurableBeanFactory) {
            return ConditionEvaluationReport.get((ConfigurableListableBeanFactory) beanFactory);
        }
        return null;
    }

    /**
     * Obtain a {@link ConditionEvaluationReport} for the specified bean factory.
     *
     * @param beanFactory the bean factory
     * @return an existing or new {@link ConditionEvaluationReport}
     */
    public static ConditionEvaluationReport get(ConfigurableListableBeanFactory beanFactory) {
        synchronized (beanFactory) {
            ConditionEvaluationReport report;
            if (beanFactory.containsSingleton(BEAN_NAME)) {
                report = beanFactory.getBean(BEAN_NAME, ConditionEvaluationReport.class);
            } else {
                report = new ConditionEvaluationReport();
                beanFactory.registerSingleton(BEAN_NAME, report);
            }
            locateParent(beanFactory.getParentBeanFactory(), report);
            return report;
        }
    }

    private static void locateParent(BeanFactory beanFactory, ConditionEvaluationReport report) {
        if (beanFactory != null && report.parent == null && beanFactory.containsBean(BEAN_NAME)) {
            report.parent = beanFactory.getBean(BEAN_NAME, ConditionEvaluationReport.class);
        }
    }

    /**
     * Record the occurrence of condition evaluation.
     *
     * @param source    the source of the condition (class or method name)
     * @param condition the condition evaluated
     * @param outcome   the condition outcome
     */
    public void recordConditionEvaluation(String source, Condition condition, ConditionOutcome outcome) {
        this.unconditionalClasses.remove(source);
        if (!this.outcomes.containsKey(source)) {
            this.outcomes.put(source, new ConditionAndOutcomes());
        }
        this.outcomes.get(source).add(condition, outcome);
        this.addedAncestorOutcomes = false;
    }

    /**
     * Records the names of the classes that have been excluded from condition evaluation.
     *
     * @param exclusions the names of the excluded classes
     */
    public void recordExclusions(Collection<String> exclusions) {
        this.exclusions.addAll(exclusions);
    }

    /**
     * Records the names of the classes that are candidates for condition evaluation.
     *
     * @param evaluationCandidates the names of the classes whose conditions will be
     *                             evaluated
     */
    public void recordEvaluationCandidates(List<String> evaluationCandidates) {
        this.unconditionalClasses.addAll(evaluationCandidates);
    }

    /**
     * Returns condition outcomes from this report, grouped by the source.
     *
     * @return the condition outcomes
     */
    public Map<String, ConditionAndOutcomes> getConditionAndOutcomesBySource() {
        if (!this.addedAncestorOutcomes) {
            this.outcomes.forEach((source, sourceOutcomes) -> {
                if (!sourceOutcomes.isFullMatch()) {
                    addNoMatchOutcomeToAncestors(source);
                }
            });
            this.addedAncestorOutcomes = true;
        }
        return Collections.unmodifiableMap(this.outcomes);
    }

    private void addNoMatchOutcomeToAncestors(String source) {
        String prefix = source + "$";
        this.outcomes.forEach((candidateSource, sourceOutcomes) -> {
            if (candidateSource.startsWith(prefix)) {
                ConditionOutcome outcome = ConditionOutcome
                        .noMatch(ConditionMessage.forCondition("Ancestor " + source).because("did not match"));
                sourceOutcomes.add(ANCESTOR_CONDITION, outcome);
            }
        });
    }

    /**
     * Returns the names of the classes that have been excluded from condition evaluation.
     *
     * @return the names of the excluded classes
     */
    public List<String> getExclusions() {
        return Collections.unmodifiableList(this.exclusions);
    }

    /**
     * Returns the names of the classes that were evaluated but were not conditional.
     *
     * @return the names of the unconditional classes
     */
    public Set<String> getUnconditionalClasses() {
        Set<String> filtered = new HashSet<>(this.unconditionalClasses);
        filtered.removeAll(this.exclusions);
        return Collections.unmodifiableSet(filtered);
    }

    /**
     * The parent report (from a parent BeanFactory if there is one).
     *
     * @return the parent report (or null if there isn't one)
     */
    public ConditionEvaluationReport getParent() {
        return this.parent;
    }

    public ConditionEvaluationReport getDelta(ConditionEvaluationReport previousReport) {
        ConditionEvaluationReport delta = new ConditionEvaluationReport();
        this.outcomes.forEach((source, sourceOutcomes) -> {
            ConditionAndOutcomes previous = previousReport.outcomes.get(source);
            if (previous == null || previous.isFullMatch() != sourceOutcomes.isFullMatch()) {
                sourceOutcomes.forEach((conditionAndOutcome) -> delta.recordConditionEvaluation(source,
                        conditionAndOutcome.getCondition(), conditionAndOutcome.getOutcome()));
            }
        });
        List<String> newExclusions = new ArrayList<>(this.exclusions);
        newExclusions.removeAll(previousReport.getExclusions());
        delta.recordExclusions(newExclusions);
        List<String> newUnconditionalClasses = new ArrayList<>(this.unconditionalClasses);
        newUnconditionalClasses.removeAll(previousReport.unconditionalClasses);
        delta.unconditionalClasses.addAll(newUnconditionalClasses);
        return delta;
    }

    /**
     * Provides access to a number of {@link ConditionAndOutcome} items.
     */
    public static class ConditionAndOutcomes implements Iterable<ConditionAndOutcome> {

        private final Set<ConditionAndOutcome> outcomes = new LinkedHashSet<>();

        public void add(Condition condition, ConditionOutcome outcome) {
            this.outcomes.add(new ConditionAndOutcome(condition, outcome));
        }

        /**
         * Return {@code true} if all outcomes match.
         *
         * @return {@code true} if a full match
         */
        public boolean isFullMatch() {
            for (ConditionAndOutcome conditionAndOutcomes : this) {
                if (!conditionAndOutcomes.getOutcome().isMatch()) {
                    return false;
                }
            }
            return true;
        }

        @Override
        public Iterator<ConditionAndOutcome> iterator() {
            return Collections.unmodifiableSet(this.outcomes).iterator();
        }

    }

    /**
     * Provides access to a single {@link Condition} and {@link ConditionOutcome}.
     */
    public static class ConditionAndOutcome {

        private final Condition condition;

        private final ConditionOutcome outcome;

        public ConditionAndOutcome(Condition condition, ConditionOutcome outcome) {
            this.condition = condition;
            this.outcome = outcome;
        }

        public Condition getCondition() {
            return this.condition;
        }

        public ConditionOutcome getOutcome() {
            return this.outcome;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null || getClass() != obj.getClass()) {
                return false;
            }
            ConditionAndOutcome other = (ConditionAndOutcome) obj;
            return (ObjectUtils.nullSafeEquals(this.condition.getClass(), other.condition.getClass())
                    && ObjectUtils.nullSafeEquals(this.outcome, other.outcome));
        }

        @Override
        public int hashCode() {
            return this.condition.getClass().hashCode() * 31 + this.outcome.hashCode();
        }

        @Override
        public String toString() {
            return this.condition.getClass() + " " + this.outcome;
        }

    }

    private static class AncestorsMatchedCondition implements Condition {

        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            throw new UnsupportedOperationException();
        }

    }

}

